/*
 * Copyright (c) 2012 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

/*
 * 
Script to change the price and/or availability settings for your items in 
Merchant Center using the express update mechanism:

https://developers.google.com/commerce-search/docs/feeds#processing

Normal feed processing may take up to 24 hours but if you *just* change the
price or availability of an existing item, the updates go though to serving
much faster.  The trick is to update all the fields in an already serving items
exactly except for the price or availability settings.

This script accepts a list of itemID numbers and the new price/availability 
settings.  

Each item in the list is downloaded from Merchant Center usign the ContentAPI-batch-query
protocol (which ensures that all the fields you uploaded earlier is returned).

For each item, either the same data that was downloaded using the query is 
again uploaded with the new price or availability set.


REFERENCE : 
https://developers.google.com/shopping-content/getting-started/usingapi-products
http://code.google.com/p/gcs-admin-toolkit/wiki/GCSAuthentication
https://developers.google.com/accounts/docs/OAuth2#serviceaccount
http://code.google.com/p/gcs-admin-toolkit/
http://code.google.com/p/gcs-admin-toolkit/source/browse/trunk/src/auth/

USAGE:
    (first get an oauth2 private key and associate the oauth2 as an 
    administrator to your MC instance).  Specify your merchantID as the second param
    Acquire an oauth2 bearer token and call the update function.
    NOTE: oauth tokens expire after 1hour
    
		Express e = new Express("1/sZhMVnUV0BBctBiUEE6tmIgC7EfRskNBQcOiXraQg5g","8437442");
		ArrayList<SimpleEntry<String, String>> items_to_update = new ArrayList<SimpleEntry<String, String>>();
		items_to_update.add(new SimpleEntry<String,String>("online:en:us:3","30.0"));
		items_to_update.add(new SimpleEntry<String,String>("online:en:us:32123","out of stock"));		
		e.update(items_to_update);
 */

import java.util.*;
import java.util.AbstractMap.SimpleEntry;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;

import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;
import java.net.URLDecoder;

import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class Express {

	private String AUTH_TOKEN;
	private String cid;
	
	private String[] VALID_AVAILABILITIES = {"in stock", "out of stock", "limited availability", 
											 "available for order", "preorder"};
	
	public static void main(String[] args) {
		Express e = new Express("<YOUR AUTH TOKEN>","<YOUR MERCHANT ID>");
		ArrayList<SimpleEntry<String, String>> items_to_update = new ArrayList<SimpleEntry<String, String>>();
		items_to_update.add(new SimpleEntry<String,String>("online:en:us:<ITEM1 ID>","30.0"));
		items_to_update.add(new SimpleEntry<String,String>("online:en:us:<ITEM2 ID>","out of stock"));		
		e.update(items_to_update);
	}
	
	public Express(String token, String cid) {
		this.AUTH_TOKEN = token;
		this.cid = cid;
	}
	
	public void update(ArrayList<SimpleEntry<String, String>> items_to_update)
	{

	    String BATCH_FEED = ("https://content.googleapis.com/content/v1/"+ this.cid + "/items/products/schema/batch");

	    // limit is arbitrary:  batch can process at most 1MB worth of data...this 100 limit is pulled out of nowhere
	    if (items_to_update.size() > 100)
	    {
	    	System.out.println("Number of items to update per batch must be less than 100");
	    }
	    
	    try
	    {
			// access_token for oauth2
	    	// https://developers.google.com/accounts/docs/OAuth2#serviceaccount
	    	// http://code.google.com/p/gcs-admin-toolkit/wiki/GCSAuthentication
	    	// NOTE: oauth2 tokens are only valid for 1 hour.  Expired tokens should be renewed.
			// set SCOPE=https://www.googleapis.com/auth/structuredcontent
			// http://code.google.com/p/gcs-admin-toolkit/source/browse/trunk/src/auth/GCS_Auth.java		    
		    BATCH_FEED = ("https://content.googleapis.com/content/v1/"+ this.cid + "/items/products/schema/batch?access_token=" + this.AUTH_TOKEN);
	    	
	    	
		    HttpURLConnection connection = (HttpURLConnection)(new URL(BATCH_FEED)).openConnection();    
		    connection.setDoInput(true);
		    connection.setDoOutput(true);
		    connection.setRequestMethod("POST");
		    connection.setRequestProperty("Content-Type", "application/atom+xml");
		    connection.setRequestProperty("GData-Version", "1");
		    
		    // For client login
		    // https://developers.google.com/accounts/docs/AuthForInstalledApps
		    // http://code.google.com/p/gcs-admin-toolkit/source/browse/trunk/src/apiinsert/ClientLogin.java
		    // connection.setRequestProperty("Authorization", "GoogleLogin auth=" + this.ACCESS_TOKEN);
		    		    
		    String batch_template_start = ("<feed xmlns='http://www.w3.org/2005/Atom' " + 
		    							   "xmlns:batch='http://schemas.google.com/gdata/batch'>");
		    String batch_template_end = "</feed>";
		    
		    String query_string = "";
		    
		    // For each item, create a batch query stanza
		    for ( SimpleEntry<String, String> item : items_to_update) 
		        query_string =  query_string + ("<entry><batch:operation type='QUERY'/><batch:id>" + 
		        		item.getKey() + "</batch:id><id>http://content.apis.google.com/content/v1/" + 
		        		this.cid + "/items/products/schema/" + item.getKey() + "</id></entry>");   	
		    
		    query_string = batch_template_start +  query_string + batch_template_end;
		    
			InputStream qis = new ByteArrayInputStream(query_string.getBytes());
		    
			// Now post the query as a batch request
		    OutputStream outputStream = connection.getOutputStream();
		    byte[] buf = new byte[1024];
		    int numRead;
		    while ( (numRead = qis.read(buf) ) >= 0) {
		    	outputStream.write(buf, 0, numRead);
		    }    
		    outputStream.close();
		  
		    int responseCode = connection.getResponseCode();
		    //System.out.println("HTTP RESPONSE CODE: " + responseCode);
		    InputStream inputStream;
		    if (responseCode == HttpURLConnection.HTTP_OK) {
		      inputStream = connection.getInputStream();
		      
		    } else {
		      inputStream = connection.getErrorStream();
		      
		      System.out.println("ERROR IN Query: " + this.IStoString(inputStream));
		      System.exit(0);
		    }
		    
		    String q_response =  this.IStoString(inputStream);
		    //System.out.println("Query Response: " + q_response);
		    
		    
		    //parse out the response, extract the data and remove the unnecessary nodes
		    DocumentBuilderFactory docBF = DocumentBuilderFactory.newInstance();
		    docBF.setNamespaceAware(true);
		    DocumentBuilder docBuilder = (DocumentBuilder) docBF.newDocumentBuilder();
		    
		    Document doc = docBuilder.parse(new ByteArrayInputStream( q_response.getBytes( "UTF-8" ) ));
		    doc.getDocumentElement().normalize();

		    //create a namespace context to work with
			NamespaceContext ctx = new NamespaceContext()
			{
				public String getNamespaceURI(String prefix) {
					String uri;
					if (prefix.equals("openSearch"))
						uri = "http://a9.com/-/spec/opensearchrss/1.0/";
					else if (prefix.equals("sc"))
						uri = "http://schemas.google.com/structuredcontent/2009";
					else if (prefix.equals("atom"))
						uri = "http://www.w3.org/2005/Atom";
					else if (prefix.equals("app"))
						uri = "http://www.w3.org/2007/app";					
					else if (prefix.equals("scp"))
						uri = "http://schemas.google.com/structuredcontent/2009/products";
					else if (prefix.equals("batch"))
						uri = "http://schemas.google.com/gdata/batch";	
					else if (prefix.equals("s"))
						uri = "http://www.google.com/shopping/api/schemas/2010";										
					else
						uri = null;
					return uri;
				}
				public Iterator getPrefixes(String val) {
					return null;
				}         
				public String getPrefix(String uri) {
					return null;
				}
			};

	        XPathFactory xpathFactory = XPathFactory.newInstance();
		    XPath xpath = xpathFactory.newXPath();
		    xpath.setNamespaceContext(ctx);

		    // make a node list of each offer in the response
		    NodeList nl_entry = (NodeList) xpath.evaluate("/atom:feed/atom:entry", doc, XPathConstants.NODESET);

		    // TODO: Error handling for feed error responses
	        if (nl_entry.getLength() > 0)
	        { 			   
	        	for (int i=0; i< nl_entry.getLength(); i++)
	        	{
	        		Node n_entry = nl_entry.item(i);
    				Node n_id =(Node) xpath.evaluate("atom:id", n_entry, XPathConstants.NODE);
    				
    				// extract out the batch status for this item..
    				// if the batch status says 'not found' then the ID lookup failed...log it
		            Node n_batch_status = (Node) xpath.evaluate("batch:status", n_entry, XPathConstants.NODE);
	        		NamedNodeMap np_batch = n_batch_status.getAttributes();
	        		for(int ia = 0 ; ia<np_batch.getLength() ; ia++) {
	        	        Attr attribute = (Attr)np_batch.item(ia); 
	        	        if (attribute.getName().equals("reason") && attribute.getValue().equals("item not found")) {
	        	        	System.out.println("ITEM not found : " + n_id.getTextContent());
	        	        	n_entry.getParentNode().removeChild(n_entry);
	        	        }
	        	      }
		            n_entry.removeChild(n_batch_status);    				
    				
		            // now iterate for each item in the input list
    				for ( SimpleEntry<String, String> item : items_to_update) {
    					
    					String canonical_id = "http://content.googleapis.com/content/v1/" + this.cid + "/items/products/schema/" + item.getKey(); 

    					if (URLDecoder.decode(n_id.getTextContent(),"UTF-8").equalsIgnoreCase(canonical_id))
    					{
    			            
    			            Node n_pid = (Node) xpath.evaluate("sc:id", n_entry, XPathConstants.NODE);
    			            String pid = n_pid.getTextContent();
    			            
    			            //Node n_aid = (Node) xpath.evaluate("atom:id", n_entry, XPathConstants.NODE);
   			            
    			            // check to see if the availability or price should get updated
    			            boolean availability_change = false;
    			            for (String av : this.VALID_AVAILABILITIES )
    			            	if (av.equalsIgnoreCase(item.getValue()))
    			            		availability_change = true;
    			            
    			            if (availability_change )
    			            {
    			            	// replace the availability
        			            Node n_availability = (Node) xpath.evaluate("scp:availability", n_entry, XPathConstants.NODE);
        			            n_availability.setTextContent(item.getValue());    			            	
    			            }
    			            else { 
    			            	// replace the price
        			            Node n_price = (Node) xpath.evaluate("scp:price", n_entry, XPathConstants.NODE);
        			            n_price.setTextContent(item.getValue());     			            
    			            }
    			            
    			            // now remove the extra data the contentAPI send down
    			            Node n_published = (Node) xpath.evaluate("atom:published", n_entry, XPathConstants.NODE);           
    			            n_entry.removeChild(n_published);

    			            Node n_updated = (Node) xpath.evaluate("atom:updated", n_entry, XPathConstants.NODE);           
    			            n_entry.removeChild(n_updated);
    			                    		
    			            // change the batch operation to an update
    			            Node n_batch_operation = (Node) xpath.evaluate("batch:operation", n_entry, XPathConstants.NODE);           
    			            n_batch_operation.getAttributes().getNamedItem("type").setNodeValue("update");  			            
    			            
    			            Node n_edited = (Node) xpath.evaluate("app:edited", n_entry, XPathConstants.NODE);           
    			            n_entry.removeChild(n_edited);
    			            
    			            Node n_expiration_date = (Node) xpath.evaluate("sc:expiration_date", n_entry, XPathConstants.NODE);           
    			            n_entry.removeChild(n_expiration_date);   
    			           
    			            // uncomment to always include  Google Commerce Search
    			            //Node n_appcontrol = (Node) xpath.evaluate("app:control", n_entry, XPathConstants.NODE);
    			            //Element req_dest = doc.createElement("sc:required_destination");
    			            //req_dest.setAttribute("dest", "CommerceSearch");
    			            //n_appcontrol.appendChild(req_dest);
    			            
    			            NodeList nl_status = (NodeList) xpath.evaluate("app:control/sc:status", n_entry, XPathConstants.NODESET);    
			        		Node n_control = (Node) xpath.evaluate("app:control", n_entry, XPathConstants.NODE);     			            
    				        if (nl_status.getLength() > 0)
    				        	for (int jj=0; jj< nl_status.getLength(); jj++){
    				        		NamedNodeMap np = nl_status.item(jj).getAttributes();
    				        		if (np.getNamedItem("dest").getNodeValue().equals("CommerceSearch") && np.getNamedItem("status").getNodeValue().equals("enlisted")) {
    		    			            Element req_dest = doc.createElement("sc:required_destination");
    		    			            req_dest.setAttribute("dest", "CommerceSearch");
    		    			            n_control.appendChild(req_dest);    				        				
    				        		}
    				        		n_control.removeChild(nl_status.item(jj));
    				        	}


    				        // preserve only th rel=alternate link
    			            NodeList nl_links = (NodeList) xpath.evaluate("atom:link", n_entry, XPathConstants.NODESET);           
    			            
    				        if (nl_links.getLength() > 0)
    				        {
    				        	for (int j=0; j< nl_links.getLength(); j++)
    				        	{ 		
    				        		Node n_link = nl_links.item(j);
    				        		NamedNodeMap np = n_link.getAttributes();
    				        		for(int ia = 0 ; ia<np.getLength() ; ia++) {
    				        	        Attr attribute = (Attr)np.item(ia); 
    				        	        if (attribute.getName().equals("rel") && !attribute.getValue().equals("alternate"))
    				        	        	n_entry.removeChild(n_link);
    				        	      }
    				        	}
    				        }   				        	    			                                                             			           
    					}
    				}    				
	        	}
	        }

		    String update_stmt = this.serialize(doc);
		    
		    //System.out.println("Update Statement " + update_stmt);

		    // now post the update XML 
		    
		    ByteArrayInputStream uis = new ByteArrayInputStream(update_stmt.getBytes());
		    connection = (HttpURLConnection)(new URL(BATCH_FEED)).openConnection();    
		    connection.setDoInput(true);
		    connection.setDoOutput(true);
		    connection.setRequestMethod("POST");
		    connection.setRequestProperty("Content-Type", "application/atom+xml");
		    connection.setRequestProperty("GData-Version", "1");
		    
		    outputStream = connection.getOutputStream();
		    buf = new byte[1024];

		    while ( (numRead = uis.read(buf) ) >= 0) {
		    	outputStream.write(buf, 0, numRead);
		    }    
		    outputStream.close();
		  
		    responseCode = connection.getResponseCode();
		    //System.out.println("HTTP RESPONSE CODE: " + responseCode);
		    if (responseCode == HttpURLConnection.HTTP_OK) {
		      inputStream = connection.getInputStream();
			    String u_response =  this.IStoString(inputStream);
			    //System.out.println("Update Response: " + u_response);		      
			    
			    // extract out the response and display the 'status' message for each update
			    doc = docBuilder.parse(new ByteArrayInputStream( u_response.getBytes("UTF-8") ));
			    doc.getDocumentElement().normalize();	
			    nl_entry = (NodeList) xpath.evaluate("/atom:feed/atom:entry", doc, XPathConstants.NODESET);
		        if (nl_entry.getLength() > 0)
		        {
		        	for (int i=0; i< nl_entry.getLength(); i++)
		        	{
		        		Node n_entry = nl_entry.item(i);
	    				Node n_id =(Node) xpath.evaluate("sc:id", n_entry, XPathConstants.NODE);
			            Node n_batch_status = (Node) xpath.evaluate("batch:status", n_entry, XPathConstants.NODE);
		        		NamedNodeMap np_batch = n_batch_status.getAttributes();
		        		for(int ia = 0 ; ia<np_batch.getLength() ; ia++) {
		        	        Attr attribute = (Attr)np_batch.item(ia); 
		        	        if (attribute.getName().equals("reason") )
		        	          System.out.println("Update Status: itemID " + n_id.getTextContent() + " ==> " + attribute.getValue());
		        	      }	    				
		        	}
		        }
		      
		    } else {
		      inputStream = connection.getErrorStream();		      
		      System.out.println("ERROR on Update: " + this.IStoString(inputStream));
		      System.exit(0);
		    }
		    
	    }
	    catch (Exception ex) {
	    	System.out.println("Unable to process request : " + ex);
	    }
		
	}
	
	private String serialize(Document doc) throws Exception {	        
	        TransformerFactory tfactory = TransformerFactory.newInstance();
	        Transformer serializer;
	        try {
	            serializer = tfactory.newTransformer();
	            serializer.setOutputProperty(OutputKeys.INDENT, "yes");
	            serializer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
	            ByteArrayOutputStream os = new ByteArrayOutputStream();
	            serializer.transform(new DOMSource(doc), new StreamResult(os));
	            return new String(os.toByteArray());
	        } catch (TransformerException e) {
	            e.printStackTrace();	            
	            throw new RuntimeException(e);
	        }
	    }	
	  
	  private String IStoString(InputStream is) throws IOException {

	    	BufferedReader br
	        	= new BufferedReader(
	        		new InputStreamReader(is)); 
	    	StringBuilder sb = new StringBuilder(); 
	    	String line;
	    	while ((line = br.readLine()) != null) {
	    		sb.append(line);
	    	}  
	    	br.close();
	    	return sb.toString();
		  }		
}
